package mirrormonkey.rpc.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.LinkedList;

import mirrormonkey.core.module.CoreModule;
import mirrormonkey.framework.entity.EntityData;
import mirrormonkey.framework.entity.SyncEntity;
import mirrormonkey.rpc.RpcListener;
import mirrormonkey.rpc.RpcSpecification;
import mirrormonkey.rpc.annotations.InvokeLocally;
import mirrormonkey.rpc.annotations.InvokeLocally.LocalInvokeMode;
import mirrormonkey.rpc.member.RpcMethodData;
import mirrormonkey.rpc.member.RpcMethodData.RpcMethodKey;
import mirrormonkey.rpc.messages.RpcCallMessage;
import mirrormonkey.rpc.module.RegisteredCall;
import mirrormonkey.rpc.module.RpcModule;

import com.jme3.network.MessageConnection;

public class RpcInvocationHandler<T extends RpcSpecification> implements
		InvocationHandler, EntityProxy<T> {

	protected final Class<T> proxyRpcSpec;

	public final EntityProxy<T> proxy;

	public final T callTarget;

	protected final CoreModule coreModule;

	protected final RpcModule module;

	public final Collection<MessageConnection> connections;

	public final Collection<RpcListener> listeners;

	public final Collection<T> targetEntities;

	public RpcInvocationHandler(RpcModule module, Class<T> proxyRpcSpec,
			CoreModule coreModule) {
		this.proxyRpcSpec = proxyRpcSpec;
		proxy = (EntityProxy<T>) Proxy.newProxyInstance(
				proxyRpcSpec.getClassLoader(), new Class<?>[] { proxyRpcSpec,
						EntityProxy.class }, this);
		callTarget = (T) proxy;
		this.module = module;
		connections = new LinkedList<MessageConnection>();
		listeners = new LinkedList<RpcListener>();
		targetEntities = new LinkedList<T>();
		this.coreModule = coreModule;
	}

	@Override
	public T getCallTarget() {
		return callTarget;
	}

	@Override
	public void addMessageConnection(MessageConnection connection) {
		connections.add(connection);
	}

	@Override
	public void removeMessageConnection(MessageConnection connection) {
		connections.remove(connection);
	}

	@Override
	public void addRpcListener(RpcListener listener) {
		listeners.add(listener);
	}

	@Override
	public void removeRpcListener(RpcListener listener) {
		listeners.remove(listener);
	}

	@Override
	public void addTargetEntity(T entity) {
		targetEntities.add(entity);
	}

	@Override
	public void removeTargetEntity(T entity) {
		targetEntities.remove(entity);
	}

	public void checkEntitySpec(EntityData entity) {
		if (!proxyRpcSpec.isInstance(entity.entity)) {
			throw new IllegalArgumentException(
					entity.entity
							+ " does not implement this handler's specification, which is "
							+ proxyRpcSpec.getName());
		}
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args)
			throws Throwable {
		if (method.getDeclaringClass().equals(EntityProxy.class)) {
			return method.invoke(this, args);
		} else if (method.getDeclaringClass().equals(proxyRpcSpec)) {
			Object[] rpArgs = args == null ? new Object[0] : args;
			Object localResult = null;
			for (T i : targetEntities) {
				localResult = invokeOnEntity(i, method, rpArgs);
			}
			return localResult;
		} else {
			throw new IllegalArgumentException("Method not found: " + method);
		}
	}

	// TODO add warning to documentation: method will ONLY be invoked locally if
	// there is at least one connection present IF LocalInvoke is present on the
	// local implementation. method will ALWAYS be invoked locally IF
	// localInvoke is present on exactly the overridden interface method
	// declaration.
	public Object invokeOnEntity(SyncEntity entity, Method method, Object[] args)
			throws Throwable {
		EntityData entityData = entity.getData();
		boolean localBefore = false;
		boolean localAfter = false;

		InvokeLocally il = method.getAnnotation(InvokeLocally.class);
		LocalInvokeMode localMode = il == null ? LocalInvokeMode.NONE : il
				.value();
		Object invocationResult = null;
		if (localMode.equals(LocalInvokeMode.BEFORE)) {
			invocationResult = method.invoke(entity, args);
			localBefore = true;
		}
		for (MessageConnection i : connections) {
			RpcMethodData methodData = entityData.getStaticData(
					connections.iterator().next()).getData(
					new RpcMethodKey(method.getName(),
							method.getParameterTypes()), RpcMethodData.class);
			if (methodData.localInvokeBefore && !localBefore) {
				invocationResult = method.invoke(entity, args);
				localBefore = true;
			}
			RegisteredCall rc = new RegisteredCall(module.callRegistry,
					methodData, i, this, methodData.timeout, entityData.entity);

			RpcCallMessage message = new RpcCallMessage(entityData.id,
					methodData.id, rc.id, methodData.packParams(args),
					methodData.reliable,
					coreModule.getConnectionInfo(i).estimatedRemoteTime);

			i.send(message);
			if (methodData.localInvokeAfter && !localAfter) {
				invocationResult = method.invoke(entity, args);
				localAfter = true;
			}

		}
		if (localMode.equals(LocalInvokeMode.AFTER) && !localAfter) {
			invocationResult = method.invoke(entityData.entity, args);
		}
		return invocationResult;
	}
}
